/*
 * Copyright 2007,2008 Anton Dignoes, Daniel Rampanelli, Martin Palma
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.webduff.store.history;

import java.io.InputStream;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Hashtable;
import java.util.Vector;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNRevisionProperty;
import org.tmatesoft.svn.core.internal.util.SVNTimeUtil;

import com.googlecode.webduff.MimeTyper;
import com.googlecode.webduff.authentication.provider.Credential;
import com.googlecode.webduff.exceptions.ForbiddenException;
import com.googlecode.webduff.exceptions.WebdavException;
import com.googlecode.webduff.io.URI;
import com.googlecode.webduff.store.WebDuffSVNRepository;
import com.googlecode.webduff.store.WebDuffSVNRepositoryListener;
import com.googlecode.webduff.store.WebdavStore;


public class HistoryWebDuffStore implements WebdavStore, WebDuffSVNRepositoryListener {

	private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(HistoryWebDuffStore.class);
	
	private WebDuffSVNRepository theRepository;
	
	/**
	 * The cache is storing all years, months, days, hours as String values, its latest revision number as Long and not bounded in size!
	 * This means its size grows dependent on the number of revisions, its worst case size is about 73kB per year:
	 * year : 	1x4x4bytes			=     16bytes
	 * months : 12x2x4bytes			=     96bytes
	 * days : 	12x31x2x4bytes		=  2,976bytes
	 * hours : 	12x31x24x2x4bytes	= 71,424bytes
	 * r. num.: 12x31x24x16bytes    = 142,848 bytes
	 * 								  -----------
	 * sum							= 212.3 kByte (+ overhead of Hashtable References)
	 */
	private Hashtable<String, Hashtable<String, Hashtable<String, Hashtable<String, Long>>>> cache;
	/**
	 * current Revision as SVNRevisionData, since not present in the cache 
	 */
	private SVNRevisionData currentRevisionData;

	private Credential theCredential;
	
	public HistoryWebDuffStore(WebDuffSVNRepository aRepository, Credential credential) {
		theRepository = aRepository;
		theCredential = credential;

		log.trace("Build cache");
		cache = new Hashtable<String, Hashtable<String, Hashtable<String, Hashtable<String, Long>>>>();
		try {
			for(long revision = 0; revision <= theRepository.getLatestRevision() - 1; revision++) {
				Date revDate = SVNTimeUtil.parseDate(theRepository.getRevisionPropertyValue(revision, SVNRevisionProperty.DATE));
				addToCache(new SVNRevisionData(Long.valueOf(revision), revDate));
			}
			// save current revision
			long latestRevision = theRepository.getLatestRevision();
			Date lRevDate = SVNTimeUtil.parseDate(theRepository.getRevisionPropertyValue(latestRevision, SVNRevisionProperty.DATE));
			currentRevisionData = new SVNRevisionData(latestRevision, lRevDate);
		} catch (SVNException e) {
			log.debug("Exception in building cache");
			throw new WebdavException(e.getMessage());
		}
		
		// register to repository commit changes
		theRepository.addCommitListener(this);
	}
	
	private Long getRevisionByURI(URI uri) throws HistoryDateParsingException {
		GregorianCalendar theDate = new GregorianCalendar();
		Collection<String> levelReached = null;
		
		levelReached = getYears();
		
		if(uri.size() == 0) {
			throw new HistoryDateParsingException(levelReached);
		}
		
		try {
			theDate.set(Calendar.YEAR, Integer.parseInt(uri.getNthComponent(0)));
			levelReached = getMonths(theDate.get(Calendar.YEAR));
				
			if(uri.size() == 1) {
				throw new HistoryDateParsingException(levelReached);
			}
			
			theDate.set(Calendar.MONTH, Integer.parseInt(uri.getNthComponent(1)) - 1);
			levelReached = getDays(theDate.get(Calendar.YEAR), theDate.get(Calendar.MONTH) + 1);
				
			if(uri.size() == 2) {
				throw new HistoryDateParsingException(levelReached);
			}
			
			theDate.set(Calendar.DAY_OF_MONTH, Integer.parseInt(uri.getNthComponent(2)));
			levelReached = getHours(theDate.get(Calendar.YEAR), theDate.get(Calendar.MONTH) + 1, theDate.get(Calendar.DAY_OF_MONTH));
				
			if(uri.size() == 3) {
				throw new HistoryDateParsingException(levelReached);
			}
			
			theDate.set(Calendar.HOUR_OF_DAY, Integer.parseInt(uri.getNthComponent(3)));
			
		} catch(NumberFormatException e) {
			throw new HistoryDateParsingException(levelReached);
		}
			
		return getCacheRevisionByDate(theDate.getTime());
	}
	
	public void begin() {

	}

	public void commit() {
		
	}

	public void createFolder(URI uri) {
		throw new ForbiddenException();
	}

	public void createResource(URI uri) {
		throw new ForbiddenException();
	}

	public String[] getChildrenNames(URI uri) {
		log.trace("getChildrenNames(" + uri + ")");

		if(!isFolder(uri)) {
			return null;
		}
		
		if(uri.size() == 0) { // root
			Collection<String> children = getYears();
			return children.toArray(new String[children.size()]);
		}
			
		Integer year = Integer.parseInt(uri.getNthComponent(0));
		if(uri.size() == 1) {
			Collection<String> children = getMonths(year);
			return children.toArray(new String[children.size()]);
		}
			
		Integer month = Integer.parseInt(uri.getNthComponent(1));
		if(uri.size() == 2) {
			Collection<String> children = getDays(year, month);
			return children.toArray(new String[children.size()]);
		}
			
		Integer day = Integer.parseInt(uri.getNthComponent(2));
		if(uri.size() == 3) {
			Collection<String> children = getHours(year, month, day);
			return children.toArray(new String[children.size()]);
		}
			
		Integer hour = Integer.parseInt(uri.getNthComponent(3));
			
		URI physicalURI = new URI(uri);
		// remove date path
		for(int i = 0; i < 4; i++)
			physicalURI = physicalURI.stripFirst();
		try {
			Vector<String> children = theRepository.listByRevision(
				physicalURI,
				getCacheRevisionByDate(new GregorianCalendar(year, month - 1, day, hour, 59).getTime()),
				theCredential
			);
			return children.toArray(new String[children.size()]);
		} catch (SVNException e) {
			log.error("getChildrenNames : " + e);
			throw new WebdavException(e);
		}
	}
	
	public Date getCreationDate(URI uri) {
		if(uri.size() < 5)
			return new Date();
		try {
			Integer year = Integer.valueOf(uri.getNthComponent(0));
			Integer month = Integer.valueOf(uri.getNthComponent(1));
			Integer day = Integer.valueOf(uri.getNthComponent(2));
			Integer hour = Integer.valueOf(uri.getNthComponent(3));
		
			URI physicalURI = new URI(uri);
			//remove date path
			for(int i = 0; i < 4; i++)
				physicalURI = physicalURI.stripFirst();
		
			return theRepository.getCreationDateByRevision(
				physicalURI, 
				getCacheRevisionByDate(new GregorianCalendar(year, month - 1, day, hour, 59).getTime()), 
				theCredential);
		} catch(Throwable t) {
			log.debug("Exception (getCreationDate)", t);
			throw new WebdavException(t.getMessage());
		}
	}

	public Date getLastModified(URI uri) {
		if(uri.size() < 5)
			return new Date();
		try {
			Integer year = Integer.valueOf(uri.getNthComponent(0));
			Integer month = Integer.valueOf(uri.getNthComponent(1));
			Integer day = Integer.valueOf(uri.getNthComponent(2));
			Integer hour = Integer.valueOf(uri.getNthComponent(3));
		
			URI physicalURI = new URI(uri);
			//remove date path
			for(int i = 0; i < 4; i++)
				physicalURI = physicalURI.stripFirst();
		
			return theRepository.getLastModifiedByRevision(
				physicalURI, 
				getCacheRevisionByDate(new GregorianCalendar(year, month - 1, day, hour, 59).getTime()), 
				theCredential);
		} catch(Throwable t) {
			log.debug("Exception (getLastModifiedByRevision)", t);
			throw new WebdavException(t.getMessage());
		}
	}

	public InputStream getResourceContent(URI uri) {
		if(!isResource(uri))
			return null;
		
		Integer year = Integer.valueOf(uri.getNthComponent(0));
		Integer month = Integer.valueOf(uri.getNthComponent(1));
		Integer day = Integer.valueOf(uri.getNthComponent(2));
		Integer hour = Integer.valueOf(uri.getNthComponent(3));
		
		URI physicalURI = new URI(uri);
		//remove date path
		for(int i = 0; i < 4; i++)
			physicalURI = physicalURI.stripFirst();
		
		try {
			return theRepository.getResourceContentByRevision(
				physicalURI,
				getCacheRevisionByDate(new GregorianCalendar(year, month - 1, day, hour, 59).getTime()),
				theCredential
			);
		} catch(SVNException e) {
			log.debug("Exception (getResourceContent)", e);
			throw new WebdavException(e.getMessage());
		}
	}

	public long getResourceLength(URI uri) {
		if(!isResource(uri))
			return 0L;
		
		Integer year = Integer.valueOf(uri.getNthComponent(0));
		Integer month = Integer.valueOf(uri.getNthComponent(1));
		Integer day = Integer.valueOf(uri.getNthComponent(2));
		Integer hour = Integer.valueOf(uri.getNthComponent(3));
		
		
		URI physicalURI = new URI(uri);
		//remove date path
		for(int i = 0; i < 4; i++)
			physicalURI = physicalURI.stripFirst();
		
		try {
			return theRepository.getResourceLengthByRevision(
				physicalURI,
				getCacheRevisionByDate(new GregorianCalendar(year, month - 1, day, hour, 59).getTime()),
				theCredential
			);
		} catch (SVNException e) {
			log.debug("Exception (getResourceLength)", e);
			throw new WebdavException(e.getMessage());
		}
	}

	public boolean isFolder(URI uri) {
		if(!objectExists(uri)) {
			return false;
		}
		
		if(uri.size() < 5) {
			return true;
		}
		Integer year = Integer.valueOf(uri.getNthComponent(0));
		Integer month = Integer.valueOf(uri.getNthComponent(1));
		Integer day = Integer.valueOf(uri.getNthComponent(2));
		Integer hour = Integer.valueOf(uri.getNthComponent(3));
		
		
		URI physicalURI = new URI(uri);
		//remove date path
		for(int i = 0; i < 4; i++)
			physicalURI = physicalURI.stripFirst();
		
		try {
			return theRepository.isFolderByRevision(
				physicalURI,
				getCacheRevisionByDate(new GregorianCalendar(year, month - 1, day, hour, 59).getTime()),
				theCredential
			);
		} catch (SVNException e) {
			log.debug("Exception (isFolder)", e);
			throw new WebdavException(e.getMessage());
		}
	}

	public boolean isResource(URI uri) {		
		return objectExists(uri) && !isFolder(uri);
	}

	public boolean objectExists(URI uri) {
		try {
			if(uri.size() == 0) // root
				return true;
			
			Integer year = Integer.parseInt(uri.getNthComponent(0));
			if(uri.size() == 1 && getYears().contains(year.toString()))
				return true;
			
			Integer month = Integer.parseInt(uri.getNthComponent(1));
			if(uri.size() == 2 && getMonths(year).contains(month.toString()))
				return true;
			
			Integer day = Integer.parseInt(uri.getNthComponent(2));
			if(uri.size() == 3 && getDays(year, month).contains(day.toString()))
				return true;
			
			Integer hour = Integer.parseInt(uri.getNthComponent(3));
			if(uri.size() == 4 && getHours(year, month, day).contains(hour.toString()))
				return true;
			
			URI physicalURI = new URI(uri);
			//remove date path
			for(int i = 0; i < 4; i++)
				physicalURI = physicalURI.stripFirst();
			
			return theRepository.objectExistsByRevision(
				physicalURI,
				getCacheRevisionByDate(new GregorianCalendar(year, month - 1, day, hour, 59).getTime()),
				theCredential
			);
		} catch(NumberFormatException e) {
			return false;	
		} catch(NullPointerException e) {
			return false;
		} catch (SVNException e) {
			log.error("objectExists : " + e);
			throw new WebdavException(e);
		}
	}

	public void removeObject(URI uri) {
		throw new ForbiddenException();
	}

	public void rollback() {

	}

	public void setResourceContent(URI resourceUri, InputStream content, String contentType, String characterEncoding) {
		throw new ForbiddenException();
	}
	
	public boolean canCreateFolder(URI uri) {
		return false;
	}

	public boolean canCreateResource(URI uri) {
		return false;
	}

	public String getMimeType(URI uri) {
		try {
			long revision = getRevisionByURI(uri);
			URI physicalURI = new URI(uri);
			//remove date path
			for(int i = 0; i < 4; i++)
				physicalURI = physicalURI.stripFirst();
			
			return theRepository.getMimeTypeByRevision(physicalURI, revision, theCredential);
		} catch (HistoryDateParsingException e) {
			return MimeTyper.FOLDER_MIMETYPE;
		} catch (SVNException e) {
			throw new WebdavException(e);
		}
	}
	
	private Collection<String> getYears() {
		// no synchronization needed since no deletion and no upper level
		return cache.keySet();
	}
	
	private Collection<String> getMonths(Integer year) {
		synchronized(cache) { // needed since a year could by added and the months not yet, by an other thread, which would result in a NullPointer
			return cache.get(year.toString()).keySet();
		}
	}
	
	private Collection<String> getDays(Integer year, Integer month) {
		synchronized(cache) {
			return cache.get(year.toString()).get(month.toString()).keySet();
		}
	}
	
	private Collection<String> getHours(Integer year, Integer month, Integer day) {
		synchronized(cache) {
			return cache.get(year.toString()).get(month.toString()).get(day.toString()).keySet();
		}
	}
	
	private Long getCacheRevisionByDate(Date revDate) {
		Calendar revCalendar = new GregorianCalendar();
		revCalendar.setTime(revDate);
		String yearString = Integer.toString(revCalendar.get(Calendar.YEAR));
		String monthString = Integer.toString(revCalendar.get(Calendar.MONTH) + 1);
		String dayString = Integer.toString(revCalendar.get(Calendar.DAY_OF_MONTH));
		String hourString = Integer.toString(revCalendar.get(Calendar.HOUR_OF_DAY));
		return cache.get(yearString).get(monthString).get(dayString).get(hourString);
	}

	public void commitPerformed(SVNLogEntry theLogEntry) {
		log.trace("commitPerformed(" + theLogEntry + ")");
		
		// make method reentrant/idempotent
		if(currentRevisionData.revisionNumber < theLogEntry.getRevision()) {
			SVNRevisionData revision = new SVNRevisionData(Long.valueOf(theLogEntry.getRevision()), theLogEntry.getDate());
			addToCache(currentRevisionData);
			currentRevisionData = revision;
		}
	}

	public Credential getMonitoringCredential() {
		return theCredential;
	}
	
	private class SVNRevisionData {
		public Date date;
		public Long revisionNumber;
		
		public SVNRevisionData(Long rNumber, Date aDate) {
			date = aDate;
			revisionNumber = rNumber;
		}
	}
	
	private void addToCache(SVNRevisionData aRevisionData) {
		GregorianCalendar dateCalendar = new GregorianCalendar();
		dateCalendar.setTime(aRevisionData.date);
		
		synchronized(cache) {
			String yearString = Integer.toString(dateCalendar.get(Calendar.YEAR));
			if(!cache.containsKey(yearString)) {
				cache.put(yearString, new Hashtable<String, Hashtable<String, Hashtable<String, Long>>>());
			}
			
			String monthString = Integer.toString(dateCalendar.get(Calendar.MONTH) + 1);
			if(!cache.get(yearString).containsKey(monthString)) {
				cache.get(yearString).put(monthString, new Hashtable<String, Hashtable<String, Long>>());
			}
			
			String dayString = Integer.toString(dateCalendar.get(Calendar.DAY_OF_MONTH));
			if(!cache.get(yearString).get(monthString).containsKey(dayString)) {
				cache.get(yearString).get(monthString).put(dayString, new Hashtable<String, Long>());
			}
			
			String hourString = Integer.toString(dateCalendar.get(Calendar.HOUR_OF_DAY));
			if(!cache.get(yearString).get(monthString).get(dayString).contains(hourString) || !cache.get(yearString).get(monthString).get(dayString).get(hourString).equals(aRevisionData.revisionNumber)) {
				cache.get(yearString).get(monthString).get(dayString).put(hourString, aRevisionData.revisionNumber);
			}
		}
	}

}
